'''
Created on 2 Jan 2010

@author: ai
'''

import logging
logger = logging.getLogger("")
logger.setLevel(logging.DEBUG)
import os
import re
from xml.etree.ElementTree import ElementTree
from glob import glob
import base64
import cPickle as pickle

# ------------------------------------------------------------------------------
#    My modules.
# ------------------------------------------------------------------------------
import utils
# ------------------------------------------------------------------------------

CMD_HG = "hg"

class SCMRevision(object):
    """ Generic wrapper around a particular revision in a SCM.
    """
    
    # --------------------------------------------------------------------------
    #    Regular expressions to help parsing the output text of various
    #    log outputs.
    # --------------------------------------------------------------------------
    """ hg example:
    I:\eclipse_workspace\Dual N-Back>hg log
    changeset:   1:be7a526ef1b5
    tag:         tip
    user:        ai
    date:        Wed Dec 16 22:26:51 2009 +0000
    summary:     Fleshed out the initial instructions activity.
    """
    RE_REVISION_HG_CHANGESET = re.compile("\s*changeset:\s+[\d]+:(.*)")
    RE_REVISION_HG_TAG = re.compile("\s*tag:\s+(.*)")
    RE_REVISION_HG_USER = re.compile("\s*user:\s+(.*)")
    RE_REVISION_HG_DATE = re.compile("\s*date:\s+(.*)")
    RE_REVISION_HG_SUMMARY = re.compile("\s+summary:(.*)")
    
    # --------------------------------------------------------------------------
    
    def __init__(self, method, text):
        self.method = method
        if self.method == "hg":
            self._parse_hg(text)
    
    def _parse_hg(self, text):
        lines = text.splitlines()        
        self.id, self.tag, self.user, self.date = (None, None, None, None)        
        for line in lines:            
            m = self.RE_REVISION_HG_CHANGESET.match(line)
            if m is not None:
                self.id = m.group(1)
                continue
            m = self.RE_REVISION_HG_TAG.match(line)
            if m is not None:
                self.tag = m.group(1)
                continue
            m = self.RE_REVISION_HG_USER.match(line)
            if m is not None:
                self.user = m.group(1)
                continue
            m = self.RE_REVISION_HG_DATE.match(line)
            if m is not None:
                self.date = m.group(1)
                continue
        assert(self.id is not None)        
        assert(self.user is not None)
        assert(self.date is not None)        
        
    def __cmp__(self, other):
        """ Two SCMRevision objects are identical if all their member variables
        are identical.
        """
        attrs = None
        if self.method == "hg":
            attrs = ["id", "user", "date"]
        assert(attrs)
        if all(getattr(self, elem) == getattr(other, elem) for elem in attrs):
            return 0
        return -1 
    
    @staticmethod
    def convert_to_str(obj):
        return base64.b64encode((pickle.dumps(obj, -1)))
    
    @staticmethod
    def convert_from_str(s):
        return pickle.loads(base64.b64decode(s))        
        
class SCMHandler(object):
    """ Generic wrapper around a controlled source-code location.  We use this
    class to do such things as get a list of revisions, determine a unique
    identification string for a particular revision, clone a repo, etc.
    
    Also, this will handle the Android-ness of the source code, such as
    what the package name is, where to find binaries, etc.
    """
    
    RE_JAVA_PACKAGE_NAME = re.compile("\s*package\s+([^\s]+)\s*;")
    
    def __init__(self, fullpath, method):
        self.logger = logging.getLogger("SCMHandler")
        self.logger.debug("ENTRY __init__.  fullpath: \"%s\", method: \"%s\"" % (fullpath, method))
        self.fullpath = os.path.abspath(fullpath)
        assert(os.path.isdir(self.fullpath))
        self.method = method
        assert(self.method in ["hg"])
        
        # check AndroidManifest.xml exists and use it to get the package name.
        assert(os.path.isfile(os.path.join(self.fullpath, "AndroidManifest.xml")))
        tree = ElementTree()
        tree.parse(os.path.join(self.fullpath, "AndroidManifest.xml"))
        manifest = tree.getroot()
        self.package_name = manifest.attrib["package"]
        assert(self.package_name is not None)        
        
        # ----------------------------------------------------------------------
        #    Function maps.  These should be read as switch-case statements
        #    with the values as the function pointers to call for a
        #    particular SCM method.
        # ----------------------------------------------------------------------
        self.get_revisions_map = {"hg": self._get_revisions_hg}
        self.clone_to_dir_map = {"hg": self._clone_to_dir_hg}
        # ----------------------------------------------------------------------
        
    def get_revisions(self):
        return self.get_revisions_map[self.method]()
        
    def _get_revisions_hg(self):
        os.chdir(self.fullpath)
        results = utils.execute_command([CMD_HG, "log"])        
        assert("abort" not in results.splitlines()[0].lower())
        
        revisions = []
        lines = []
        for line in results.splitlines():
            if len(line.strip()) != 0:
                lines.append(line)
            else:
                revisions.append(SCMRevision("hg", '\n'.join(lines)))
                lines = [] 
        
        return revisions
    
    def clone_to_dir(self, revision, destination):
        assert(os.path.isdir(destination))
        assert(revision.id is not None)
        return self.clone_to_dir_map[self.method](revision, destination)
    
    def _clone_to_dir_hg(self, revision, destination):
        os.chdir(self.fullpath) 
        results = utils.execute_command([CMD_HG, "clone", "--updaterev", revision.id, ".", destination])
        assert("abort" not in results.splitlines()[0].lower())
        
    def get_binary_file(self):
        """ Find the single debug APK file that is the compiled version of the
        Android project under test.
        """
        binary_path = os.path.join(self.fullpath, "bin")
        assert(os.path.isdir(binary_path))
        binary_files = glob(os.path.join(binary_path, "*.apk"))
        assert(len(binary_files) > 0)
        binary_file = os.path.abspath(binary_files[0])
        assert(os.path.isfile(binary_file))
        return binary_file
    
    def get_test_names(self):
        """ Return a list of all tests that are available for running.  Do this
        by identifying those classes that extend junit.framework.TestCase.
        
        Return the names as CTagsWrapper.TestCase objects, which repr() as
        e.g. "com.project.app.tests.XXX", where XXX is the class name of the
        test.
        """       
        logger.debug("get_test_names entry")
        all_tests = []
        for root, dirs, files in os.walk(self.fullpath):
            for name in files:
                if name.endswith(".java"):
                    path = os.path.join(root, name)
                    logger.debug("considering %s..." % (path, ))
                    ct = utils.CTagsWrapper(path)
                    if ct.is_testcase_class():
                        tests = ct.get_tests()                        
                        all_tests.extend(tests)
        return all_tests                                                   
                